suite("document-node", function () {
    "use strict";

    var DocumentNode = require("./document-node");
    var assert = require("assert");

    test("accepts only valid id", function () {
        try {
            new DocumentNode(-1, 0);
            assert(false);
        } catch (error) {
            assert(error.message.indexOf("invalid id") >= 0);
        }
    });

    test("accepts only valid version", function () {
        try {
            new DocumentNode(0, -1);
            assert(false);
        } catch (error) {
            assert(error.message.indexOf("invalid version") >= 0);
        }
    });

    test("doesn't allow to change id", function () {
        var node = new DocumentNode(0, 0);
        try {
            node.id = 1;
            assert(false);
        } catch (error) {
            assert(error.message.indexOf("id cannot be changed") >= 0);
        }
    });

    test("increments version on change", function () {
        var defineType = require("./define-type");
        var Type = defineType(new DocumentNode(0, 0), {a: 0});
        var node = new Type(0, 0);
        var versionBefore = node.version;
        node.a = 2;
        assert(node.version >= versionBefore);
    });

    test("serializes only json values", function () {
        var node = new DocumentNode(0, 0);
        node.a = function () {};
        node.b = undefined;
        node.c = 1;
        var json = node.toJson();
        assert(json.c === 1);
        for (var key in json) {
            switch (key) {
            case "id":
            case "version":
            case "c":
                break;
            default:
                assert(false);
            }
        }
    });

    test("serializes document node fields", function () {
        var node = new DocumentNode(0, 0);
        node.child = new DocumentNode(0, 0);
        node.child.a = 1;
        var json = node.toJson();
        assert(json.child.a === 1);
    });

    test("serializes node list fields", function () {
        var node = new DocumentNode(0, 0);
        node.list = new DocumentNode.List;
        node.list.append(1);
        node.list.append(2);
        var json = node.toJson();
        assert(Array.isArray(json.list))
        assert(json.list.length === 2);
    });

    test("skips serializing ignored fields", function () {
        var node = new DocumentNode(0, 0);
        node.jsonOptions.a = {ignore: true};
        node.a = 1;
        node.b = 2;
        var json = node.toJson();
        assert(!("a" in json));
        assert(json.b === 2);
    });

    test("list serializes only json values", function () {
        var list = new DocumentNode.List;
        list.append(function () {});
        list.append(undefined);
        list.append(1);
        var json = list.toJson();
        assert(Array.isArray(json));
        assert(json.length === 1);
        assert(json[0] === 1);
    });

    test("list serializes values", function () {
        var list = new DocumentNode.List;
        list.append(1);
        list.append(new DocumentNode(0, 0));
        list.items[1].a = 2;
        list.append(new DocumentNode.List);
        list.items[2].append(3);
        var json = list.toJson();
        assert(Array.isArray(json));
        assert(json.length === 3);
        assert(json[0] === 1);
        assert(json[1].a === 2);
        assert(Array.isArray(json[2]));
        assert(json[2].length === 1);
        assert(json[2][0] === 3);
    });

    test("deserializes values", function () {
        var node = new DocumentNode(0, 0);
        var json = {
            id: node.id,
            version: node.version + 1,
            a: 1,
            b: 2
        };
        node.fromJson(json);
        assert(node.a === 1);
        assert(node.b === 2);
    });

    test("skips deserializing ignored fields", function () {
        var node = new DocumentNode(0, 0);
        node.jsonOptions.a = {ignore: true};
        var json = {
            id: node.id,
            version: node.version + 1,
            a: 1,
            b: 2
        };
        node.fromJson(json);
        assert(!("a" in node));
        assert(node.b === 2);
    });

    test("fails on invalid json id", function () {
        var node = new DocumentNode(1, 0);
        var json = {
            id: 2,
            version: node.version + 1
        };
        try {
            node.fromJson(json);
            assert(false);
        } catch (error) {
            assert(error.message.indexOf("invalid json source") >= 0);
        }
    });

    test("fails on invalid json version", function () {
        var node = new DocumentNode(0, 0);
        var json = {
            id: 2,
            version: -1
        };
        try {
            node.fromJson(json);
            assert(false);
        } catch (error) {
            assert(error.message.indexOf("invalid json source") >= 0);
        }
    });

    test("ignores direct fields if json has lower version", function () {
        var node = new DocumentNode(0, 1);
        node.a = 1;
        var json = {
            id: node.id,
            version: node.version - 1,
            a: 2
        };
        node.fromJson(json);
        assert(node.a === 1);
    });

    test("deserializes to json version", function () {
        var node = new DocumentNode(0, 1);
        node.a = 1;
        var json = {
            id: node.id,
            version: node.version + 3,
            a: 2
        };
        node.fromJson(json);
        assert(node.version === json.version);
    });

    test("deserializes nodes", function () {
        var node = new DocumentNode(0, 0);
        node.jsonOptions.a = {nodeType: DocumentNode};
        node.a = new DocumentNode(1, 0);
        node.a.b = 1;
        var json = {
            id: node.id,
            version: node.version + 1,
            a: {
                id: node.a.id,
                version: node.a.version + 1,
                b: 2
            }
        };
        node.fromJson(json);
        assert(node.a.b === 2);
    });

    test("deserializes child node, even if version is lower", function () {
        var node = new DocumentNode(0, 1);
        node.jsonOptions.a = {nodeType: DocumentNode};
        node.a = new DocumentNode(1, 0);
        node.a.b = 1;
        var json = {
            id: node.id,
            version: node.version - 1,
            a: {
                id: node.a.id,
                version: node.a.version + 1,
                b: 2
            }
        };
        node.fromJson(json);
        assert(node.a.b === 2);
    });

    test("skips child node if version is lower", function () {
        var node = new DocumentNode(0, 1);
        node.jsonOptions.a = {nodeType: DocumentNode};
        node.a = null;
        var json = {
            id: node.id,
            version: node.version - 1,
            a: {
                id: 1,
                version: 1,
                b: 2
            }
        };
        node.fromJson(json);
        assert(node.a === null);
    });

    test("overwrites child node if different id", function () {
        var node = new DocumentNode(0, 0);
        node.jsonOptions.a = {nodeType: DocumentNode};
        node.a = new DocumentNode(1, 0);
        node.a.b = 1;
        node.a.c = 2;
        var json = {
            id: node.id,
            version: node.version + 1,
            a: {
                id: 2,
                version: 0,
                d: 3,
                e: 4
            }
        };
        node.fromJson(json);
        assert(node.a.id === 2);
        assert(node.a.d === 3);
        assert(node.a.e === 4);
    });

    test("fails if not an array", function () {
        var list = new DocumentNode.List;
        try {
            list.fromJson(1);
            assert(false);
        } catch (error) {
            assert(error.message.indexOf("invalid json source") >= 0);
        }
    });

    test("deserializes items", function () {
        var list = new DocumentNode.List([1, 2, 3]);
        list.fromJson([4, 5, 6, 7]);
        assert(list.items.length === 4);
        assert(list.items[0] === 4);
        assert(list.items[1] === 5);
        assert(list.items[2] === 6);
        assert(list.items[3] === 7);
    });

    test("fails deserializing list when json not array", function () {
        var list = new DocumentNode.List;
        try {
            list.fromJson(1);
            assert(false);
        } catch (error) {
            assert(error.message.indexOf("invalid json source") >= 0);
        }
    });

    test("fails deserializing child list when json not array", function () {
        var node = new DocumentNode(0, 0);
        node.jsonOptions.list = {list: true};
        var json = {
            id: node.id,
            version: node.version + 1,
            list: 1
        };
        try {
            node.fromJson(json);
            assert(false);
        } catch (error) {
            assert(error.message.indexOf("invalid json source") >= 0);
        }
    });

    test("creates a new child list if doesn't exist", function () {
        var node = new DocumentNode(0, 0);
        node.jsonOptions.list = {list: true};
        var json = {
            id: node.id,
            version: node.version + 1,
            list: []
        };
        node.fromJson(json);
        assert(node.list instanceof DocumentNode.List);
    });

    test("deserializes child list", function () {
        var node = new DocumentNode(0, 0);
        node.jsonOptions.list = {list: true};
        node.list = new DocumentNode.List;
        var json = {
            id: node.id,
            version: node.version + 1,
            list: [1, 2, 3]
        };
        node.fromJson(json);
        assert(node.list.items.length === 3);
        assert(node.list.items[0] === 1);
        assert(node.list.items[1] === 2);
        assert(node.list.items[2] === 3);
    });

    test("deserializes child list of nodes, reseting", function () {
        var node = new DocumentNode(0, 0);
        node.jsonOptions.list = {
            list: true,
            nodeType: DocumentNode
        };
        node.list = new DocumentNode.List;
        node.list.append(new DocumentNode(1, 0));
        node.list.append(new DocumentNode(2, 0));
        node.list.append(new DocumentNode(3, 0));
        var json = {
            id: node.id,
            version: node.version + 1,
            list: [{
                id: 4,
                version: 0
            }, {
                id: 5,
                version: 0
            }]
        };
        node.fromJson(json);
        assert(node.list.items.length === 2);
        assert(node.list.items[0] instanceof DocumentNode);
        assert(node.list.items[0].id === 4);
        assert(node.list.items[1] instanceof DocumentNode);
        assert(node.list.items[1].id === 5);
    });

    test("deserializes child list of nodes, preserving", function () {
        var node = new DocumentNode(0, 1);
        node.jsonOptions.list = {
            list: true,
            nodeType: DocumentNode
        };
        node.list = new DocumentNode.List;
        node.list.append(new DocumentNode(1, 0));
        node.list.append(new DocumentNode(2, 0));
        node.list.items[1].a = 1;
        var json = {
            id: node.id,
            version: node.version - 1,
            list: [{
                id: 2,
                version: node.list.items[1].version + 1,
                a: 2
            }, {
                id: 3,
                version: 0
            }, {
                id: 4,
                version: 0
            }]
        };
        node.fromJson(json);
        assert(node.list.items.length === 2);
        assert(node.list.items[0].id === 1);
        assert(node.list.items[1].id === 2);
        assert(node.list.items[1].a === 2);
    });

    test("derived types", function () {
        var defineType = require("./define-type");

        var Type0 = defineType(new DocumentNode(0, 0), {a: 0});
        var Type1 = defineType(new DocumentNode(0, 0), {
            jsonOptions: {
                b: {
                    nodeType: Type0
                },
                c: {
                    list: true,
                    nodeType: Type0
                }
            }
        });

        var o = new Type1(0, 1);
        o.b = new Type0(1, 0);
        o.b.a = 1;
        o.c = new DocumentNode.List;
        o.c.append(new Type0(3, 0));
        o.c.items[0].a = 2;
        o.c.append(new Type0(4, 0));
        o.c.items[1].a = 3;

        var json = o.toJson();
        assert(json.id === o.id);
        assert(json.version === o.version);
        assert(json.b.id === o.b.id);
        assert(json.b.version === o.b.version);
        assert(json.b.a === o.b.a);
        assert(json.c.length === 2);
        assert(json.c[0].id === o.c.items[0].id);
        assert(json.c[0].version === o.c.items[0].version);
        assert(json.c[0].a === o.c.items[0].a);
        assert(json.c[1].id === o.c.items[1].id);
        assert(json.c[1].version === o.c.items[1].version);
        assert(json.c[1].a === o.c.items[1].a);

        json = {
            id: o.id,
            version: o.version - 1,
            b: {
                id: o.b.id,
                version: o.b.version + 1,
                a: 4
            },
            c: [{
                id: o.c.items[1].id,
                version: o.c.items[1].version + 1,
                a: 5
            }]
        };

        o.fromJson(json);
        assert(o.b.a === json.b.a);
        assert(o.c.items.length === 2);
        assert(o.c.items[1].a === json.c[0].a);
    });

    test("doc node with alternating type", function () {
        var defineType = require("./define-type");

        var TextNode = defineType(new DocumentNode(0, 0), {text: ""});
        var ImageNode = defineType(new DocumentNode(0, 0), {src: ""});
        var node = new DocumentNode(0, 0);
        node.jsonOptions.child = {
            getNodeType: function (json) {
                return "src" in json ? ImageNode : TextNode;
            }
        };

        var json = {
            id: node.id,
            version: node.version + 1,
            child: {
                id: 1,
                version: 0,
                text: "some text"
            }
        };
        node.fromJson(json);
        assert(node.child instanceof TextNode);
        assert(node.child.text === json.child.text);

        json = {
            id: node.id,
            version: node.version + 1,
            child: {
                id: 2,
                version: 0,
                src: "some url"
            }
        };
        node.fromJson(json);
        assert(node.child instanceof ImageNode);
        assert(node.child.src === json.child.src);
    });

    test("child list item with alternating type", function () {
        var defineType = require("./define-type");

        var TextNode = defineType(new DocumentNode(0, 0), {text: ""});
        var ImageNode = defineType(new DocumentNode(0, 0), {src: ""});
        var node = new DocumentNode(0, 0);
        node.jsonOptions.list = {
            list: true,
            getNodeType: function (json) {
                return "src" in json ? ImageNode : TextNode;
            }
        };
        node.list = new DocumentNode.List;
        var json = {
            id: node.id,
            version: node.version + 1,
            list: [{
                id: 1,
                version: 0,
                src: "some url"
            }, {
                id: 2,
                version: 0,
                text: "some text"
            }]
        };
        node.fromJson(json);
        assert(node.list.items.length === 2);
        assert(node.list.items[0] instanceof ImageNode);
        assert(node.list.items[0].src === json.list[0].src);
        assert(node.list.items[1] instanceof TextNode);
        assert(node.list.items[1].text === json.list[1].text);
    });

    test("preserve instances by id", function () {
        require("./assign");

        var node = new DocumentNode(0, 0);
        Object.assign(node.jsonOptions, {
            a: {
                nodeType: DocumentNode
            },
            b: {
                nodeType: DocumentNode
            },
            list: {
                list: true,
                nodeType: DocumentNode
            }
        });

        var json = {
            id: node.id,
            version: node.version + 1,
            a: {
                id: 1,
                version: 0
            },
            b: {
                id: 2,
                version: 0
            },
            list: [{
                id: 1,
                version: 0
            }, {
                id: 2,
                version: 0
            }]
        };

        node.fromJson(json);
        assert(node.a === node.list.items[0]);
        assert(node.b === node.list.items[1]);
    });

    test("serialize to id", function () {
        var node = new DocumentNode(0, 0);
        node.jsonOptions.a = {
            nodeType: DocumentNode,
            idOnly: true
        };
        node.a = new DocumentNode(1, 0);
        var json = node.toJson();
        assert(json.aId === node.a.id);
        assert(!("a" in json));
    });

    test("serialize list to id", function () {
        var node = new DocumentNode(0, 0);
        node.jsonOptions.list = {
            list: true,
            idOnly: true
        };
        node.list = new DocumentNode.List;
        node.list.append(new DocumentNode(1, 0));
        node.list.append(new DocumentNode(2, 0));
        var json = node.toJson();
        assert(json.list.length === 2);
        assert(json.list[0] === node.list.items[0].id);
        assert(json.list[1] === node.list.items[1].id);
    });

    test("fail deserializing from id, when node not found", function () {
        var node = new DocumentNode(0, 0);
        Object.assign(node.jsonOptions, {
            a: {
                idOnly: true,
                nodeType: DocumentNode
            },
            list: {
                list: true,
                nodeType: DocumentNode
            }
        });

        var json = {
            id: node.id,
            version: node.version + 1,
            aId: "1",
            list: []
        };

        try {
            node.fromJson(json);
            assert(false);
        } catch (error) {
            assert(error.message.indexOf("invalid json source") >= 0);
        }
    });

    test("deserialize from id", function () {
        var node = new DocumentNode(0, 0);
        Object.assign(node.jsonOptions, {
            a: {
                idOnly: true,
                nodeType: DocumentNode
            },
            list: {
                list: true,
                nodeType: DocumentNode
            }
        });

        var json = {
            id: node.id,
            version: node.version + 1,
            aId: "1",
            list: [{
                id: 1,
                version: 0
            }]
        };

        node.fromJson(json);
        assert(node.a === node.list.items[0]);
    });

    test("fail on repeated item", function () {
        var node = new DocumentNode(0, 0);
        node.a = new DocumentNode(1, 0);
        node.b = new DocumentNode(1, 0);
        try {
            node.toJson();
            assert(false);
        } catch (error) {
            assert(error.message.indexOf("invalid document tree") >= 0);
        }
    });

    test("fail on duplicate ids", function () {
        var node = new DocumentNode(0, 0);
        node.jsonOptions.a = {
            nodeType: DocumentNode
        };
        var json = {
            id: node.id,
            version: node.version + 1,
            a: {
                id: node.id,
                version: 0
            }
        };
        try {
            node.fromJson(json);
            assert(false);
        } catch (error) {
            assert(error.message.indexOf("invalid document tree") >= 0);
        }
    });

    test("creates node with new id", function () {
        var defineType = require("./define-type");

        var Type = defineType(new DocumentNode(0, 0));

        var node0 = DocumentNode.create(Type);
        var node1 = DocumentNode.create(Type);

        assert(node1.id > node0.id);
    });

    test("serializes id only field when null", function () {
        var node = new DocumentNode(0, 0);
        node.jsonOptions.a = {
            nodeType: DocumentNode,
            idOnly: true
        };
        node.a = null;
        var json = node.toJson();
        assert(!("a" in json));
        assert(!("aId" in json));
    });

    test("deserializes when id only not defined", function () {
        var node = new DocumentNode(0, 0);
        node.jsonOptions.a = {
            nodeType: DocumentNode,
            idOnly: true
        };
        var json = {
            id: node.id,
            version: node.version + 1
        };
        node.fromJson(json);
        assert(!("a" in node));
    });

    test("deserializing list preserves list items", function () {
        var node = new DocumentNode(0, 0);
        node.jsonOptions.list = {
            list: true,
            nodeType: DocumentNode
        };
        node.list = new DocumentNode.List;
        var child = new DocumentNode(1, 0);
        node.list.append(
            child,
            new DocumentNode(2, 0));

        var json = {
            id: node.id,
            version: node.version + 1,
            list: [{
                id: node.list.items[0].id,
                version: node.list.items[0].version + 1,
                a: 1
            }, {
                id: 3,
                version: 0
            }]
        };

        node.fromJson(json);
        assert(node.list.items.length === 2);
        assert(node.list.items[0] === child);
        assert(node.list.items[1].id === json.list[1].id);
    });

    test("triggers change event after deserializing", function (done) {
        var defineType = require("./define-type");

        var Type0 = defineType(function () {
            this.list = new DocumentNode.List;
        }, new DocumentNode(0, 0), {
            jsonOptions: {
                list: {
                    list: true,
                    nodeType: DocumentNode
                }
            },

            list: null
        });

        var Type1 = defineType(new DocumentNode(0, 0), {
            a: 0
        });

        var node = new Type0(0, 0);
        node.list.append(new Type1(1, 0));
        node.list.append(new Type1(2, 0));

        var called = false;
        node.on("change", function (e) {
            if (!called) {
                called = true;
                done();
            }
        });

        var json = {
            id: 0,
            version: node.version + 1,

            list: [{
                id: 1,
                version: node.list.items[0].version + 1,
                a: 1
            }]
        };

        node.fromJson(json);
        setTimeout(function () {
            if (!called) {
                assert(false, "not triggered");
            }
        }, 30);
    });

    test("triggers list change event after deserializing", function (done) {
        var defineType = require("./define-type");

        var Type0 = defineType(function () {
            this.list = new DocumentNode.List;
        }, new DocumentNode(0, 0), {
            jsonOptions: {
                list: {
                    list: true,
                    nodeType: DocumentNode
                }
            },

            list: null
        });

        var Type1 = defineType(new DocumentNode(0, 0), {
            a: 0
        });

        var node = new Type0(0, 0);
        node.list.append(new Type1(1, 0));
        node.list.append(new Type1(2, 0));

        var called = false;
        node.list.on("change", function (e) {
            if (!called) {
                called = true;
                done();
            }
        });

        var json = {
            id: 0,
            version: node.version + 1,

            list: [{
                id: 1,
                version: node.list.items[0].version + 1,
                a: 1
            }]
        };

        node.fromJson(json);
        setTimeout(function () {
            if (!called) {
                assert(false, "not triggered");
            }
        }, 30);
    });

    test("complex object deserializing and serializing from/to the same json", function () {
        assert(false);
    });
});
